iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0
自我挑戰組

模仿知名網站的外觀系列 第 17

【Day17】模仿知名網站的外觀 X(4) 登入表單

  • 分享至 

  • xImage
  •  

在Components資料夾,建立Button.tsx,用來構建一個按鈕物件。

'use client'

interface ButtonProps{
    label: string;
    secondary?: boolean;
    fullWidth?: boolean;
    large?: boolean;
    onClick: () => void;
    disabled?: boolean;
    outline?: boolean;
}

const Button: React.FC<ButtonProps> = ({
    label,
    secondary,
    fullWidth,
    large,
    onClick,
    disabled,
    outline
}) => {
  return (
    <button disabled={disabled} onClick={onClick} className={`
        disabled:opacity-70 disabled:cursor-not-allowed rounded-full font-semibold hover:opacity-80 transition border-2
        ${fullWidth ? "w-full" : "w-fit"}
        ${secondary ? "bg-white" : "bg-sky-500"}
        ${secondary ? "text-black" : "text-white"}
        ${secondary ? "border-black" : "border-sky-500"}
        ${large ? "text-xl" : "text-md"}
        ${large ? "px-5" : "px-4"}
        ${large ? "py-3" : "py-2"}
        ${outline ? "bg-transparent" : ""}
        ${outline ? "border-white" : ""}
        ${outline ? "text-white" : ""}
    `}>{label}</button>
  )
}

export default Button

在Components資料夾,建立Modal.tsx,用來構建一個登入或註冊視窗。

'use client'

import { useCallback } from "react";
import { AiOutlineClose } from "react-icons/ai";
import Button from "./Button";

interface ModalProps {
	isOpen?: boolean;
	onClose: () => void;
	onSubmit: () => void;
	title?: string;
	body?: React.ReactElement;
	footer?: React.ReactElement;
	actionLabel: string;
	disabled?: boolean;
}

const Modal: React.FC<ModalProps> = ({
	isOpen,
	onClose,
	onSubmit,
	title,
	body,
	footer,
	actionLabel,
	disabled,
}) => {
	const handleClose = useCallback(() => {
		if (disabled) {
			return;
		}

		onClose();
	}, [disabled, onClose]);

	const handleSubmit = useCallback(() => {
		if (disabled) {
			return;
		}

		onSubmit();
	}, [disabled, onSubmit]);

	if (!isOpen) {
		return null;
	}
	return (
		<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none bg-neutral-800 bg-opacity-70">
			<div className="relative w-full lg:w-3/6 my-6 mx-auto lg: max-w-3xl h-full lg:h-auto">
				<div className="h-full lg:h-auto border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-black outline-none focus:outline-none">
					<div className="flex items-center justify-between p-10 rounded-t">
						<h3 className="text-3xl font-semibold text-white">{title}</h3>
						<button
							onClick={handleClose}
							className="p-1 ml-auto bottom-0 text-white hover:opacity-70 transition"
						>
							<AiOutlineClose size={20} />
						</button>
					</div>
					<div className="relative p-10 flex-auto">{body}</div>
					<div className="flex flex-col gap-2 p-10">
						<Button
							disabled={disabled}
							label={actionLabel}
							secondary
							fullWidth
							large
							onClick={handleSubmit}
						/>
                        {footer}
					</div>
				</div>
			</div>
		</div>
	);
};

export default Modal;

最後修改_app.tsx,顯示我們剛寫好的視窗和按鈕。

import Layout from "@/Components/Layout"
import Modal from "@/Components/Modal"
import '@/styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Modal actionLabel="Submit" isOpen title="Test Modal"/>
      <Component {...pageProps} />
    </Layout>
  )
}

啟動專案,可以看到這個視窗,這些按鈕還沒有功能點下去沒有反應。

Untitled

我們看完視窗呈現的狀態後,將layout.tsx恢復原狀。

import Layout from "@/Components/Layout"
import '@/styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}

在Components資料夾中,建立Input.tsx,用來構成登入表格中的輸入欄位。

interface InputProps{
    placeholder?: string;
    value?: string;
    type?: string;
    disabled?: boolean;
    onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

const Input: React.FC<InputProps> = ({
    placeholder,
    value,
    type,
    disabled,
    onChange
}) => {
  return (
    <input
    disabled={disabled}
    onChange={onChange}
    value={value}
    placeholder={placeholder}
    type={type}
    className="w-full p-4 text-lg bg-black border-2 border-neutral-800 rounded-md outline-none text-white focus:border-2 transition disabled:bg-neutral-900 disabled:opacity-70 disabled:cursor-not-allowed"
     />
  )
}

export default Input

接下來安裝zustand,用一種簡單直觀的方式管理應用程序的state,因為我們的登入表單的資料由一個個component所組成,我們需要有一個東西能夠方便的管理裏面的內容以及設定。

在根目錄下,建立Hooks資料夾。

在Hooks資料夾下,新增useLoginModal.tsx,將LoginModal登入表單預設為不顯示,觸發onOpen事件時會顯示表單,觸發onClose事件會關閉表單。

import { create } from "zustand";

interface LoginModalStore{
    isOpen: boolean;
    onOpen: () => void;
    onClose: () => void;
}

const useLoginModal = create<LoginModalStore>((set) => ({
    isOpen: false,
    onOpen: () => set({isOpen: true}),
    onClose: () => set({isOpen: false}),
}));

export default useLoginModal;

在Components資料夾下,新增Modals資料夾。

在Modals資料夾下,新增LoginModal.tsx,我們使用了Input和Modal來組成我們的登入表單。

'use client'

import useLoginModal from "@/Hooks/useLoginModal";
import { useCallback, useState } from "react";
import Input from "../Input";
import Modal from "../Modal";

const LoginModal = () => {
	const loginModal = useLoginModal();

	const [email, setEmail] = useState("");
	const [password, setPassword] = useState("");
	const [isLoading, setIsLoading] = useState(false);

	const onSubmit = useCallback(async () => {
		try {
			setIsLoading(true);

			loginModal.onClose();
		} catch (error) {
			console.log(error);
		} finally {
			setIsLoading(false);
		}
	}, [loginModal]);

	const bodyContent = (
		<div className="flex flex-col gap-4">
			<Input
				placeholder="Email"
				onChange={(e) => setEmail(e.target.value)}
				value={email}
				disabled={isLoading}
			/>
			<Input
				placeholder="Password"
				onChange={(e) => setPassword(e.target.value)}
				value={password}
				disabled={isLoading}
			/>
		</div>
	);
	return (
		<Modal
			disabled={isLoading}
			isOpen={loginModal.isOpen}
			title="Login"
			actionLabel="Sign in"
			onClose={loginModal.onClose}
			onSubmit={onSubmit}
			body={bodyContent}
		/>
	);
};

export default LoginModal;

為了看登入表單的外觀如何,我們對_app.tsx和useLoginModal.tsx進行修改。

_app.tsx

import Layout from "@/Components/Layout"
import LoginModal from "@/Components/Modals/LoginModal"
import '@/styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <LoginModal />
      <Component {...pageProps} />
    </Layout>
  )
}

useLoginModal.tsx

import { create } from "zustand";

interface LoginModalStore{
    isOpen: boolean;
    onOpen: () => void;
    onClose: () => void;
}

const useLoginModal = create<LoginModalStore>((set) => ({
    isOpen: true,
    onOpen: () => set({isOpen: true}),
    onClose: () => set({isOpen: false}),
}));

export default useLoginModal;

我們的登入表單可以輸入Email和密碼,現在按下表單右上角的X可以關閉表單了。

Untitled

觀察變化結束後,把useLoginModal.tsx改回來。

useLoginModal.tsx

import { create } from "zustand";

interface LoginModalStore{
    isOpen: boolean;
    onOpen: () => void;
    onClose: () => void;
}

const useLoginModal = create<LoginModalStore>((set) => ({
    isOpen: false,
    onOpen: () => set({isOpen: true}),
    onClose: () => set({isOpen: false}),
}));

export default useLoginModal;

上一篇
【Day16】模仿知名網站的外觀 X(3) 完成首頁的外觀
下一篇
【Day18】模仿知名網站的外觀 X(5) 註冊表單
系列文
模仿知名網站的外觀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言